use anyhow::{Context, Result, bail};
use base64::{Engine as _, engine::general_purpose};
use clap::{Arg, Command as ClapCommand};
use octocrab::Octocrab;
use sha2::{Digest, Sha256};
use std::env;
use std::path::Path;
use std::process::Command;
use tokio::fs;

#[derive(Debug, Clone)]
#[allow(unused)]
struct GitHubConfig {
    token: String,
    owner: String,
    repo: String,
}

impl GitHubConfig {
    fn from_env() -> Result<Self> {
        let token =
            env::var("GITHUB_TOKEN").context("GITHUB_TOKEN environment variable not set")?;
        let owner = env::var("GITHUB_OWNER").unwrap_or_else(|_| "HelixDB".to_string());
        let repo = env::var("GITHUB_REPO").unwrap_or_else(|_| "helix-db".to_string());

        Ok(GitHubConfig { token, owner, repo })
    }
}

fn generate_error_hash(error_type: &str, error_message: &str, _test_name: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(format!(
        "{}:{}",
        error_type,
        error_message.lines().take(5).collect::<Vec<_>>().join("\n")
    ));
    let hash = hasher.finalize();
    general_purpose::STANDARD.encode(hash)[0..12].to_string()
}

#[allow(unused)]
async fn check_issue_exists(github_config: &GitHubConfig, error_hash: &str) -> Result<bool> {
    println!("DEBUG: Checking if issue exists with hash: {error_hash}");

    let octocrab = Octocrab::builder()
        .personal_token(github_config.token.clone())
        .build()?;

    let search_query = format!(
        "repo:{}/{} is:issue ERROR_HASH:{}",
        github_config.owner, github_config.repo, error_hash
    );

    println!("DEBUG: GitHub search query: {search_query}");

    let issues = octocrab
        .search()
        .issues_and_pull_requests(&search_query)
        .send()
        .await?;

    let count = issues.total_count.unwrap_or(0);
    println!("DEBUG: Found {count} existing issues");

    Ok(count > 0)
}

#[allow(unused)]
async fn create_github_issue(
    github_config: &GitHubConfig,
    error_type: &str,
    error_message: &str,
    test_name: &str,
    error_hash: &str,
    query: &str,
    schema: &str,
    generated_rust_code: &str,
) -> Result<()> {
    println!(
        "DEBUG: Creating GitHub issue for {}/{}",
        github_config.owner, github_config.repo
    );

    let octocrab = Octocrab::builder()
        .personal_token(github_config.token.clone())
        .build()?;

    let title = format!("Auto-generated: {error_type} Error in {test_name}");

    let body = format!(
        "## Automatic Error Report\n\n\
        **Error Type:** {error_type}\n\
        **Test:** {test_name}\n\
        **Error Hash:** ERROR_HASH:{error_hash}\n\n\
        ### Query\n\
        ```js\n{query}\n```\n\n\
        ### Schema\n\
        ```js\n{schema}\n```\n\n\
        ### Generated Rust Code\n\
        ```rust\n{generated_rust_code}\n```\n\n\
        ### Error Details\n\
        ```\n{error_message}\n```\n\n\
        ---\n\
        *This issue was automatically generated by the hql-tests runner.*"
    );

    let labels = vec![
        "bug".to_string(),
        "automated".to_string(),
        "hql-tests".to_string(),
    ];

    println!("DEBUG: Issue title: {title}");
    println!("DEBUG: Issue body length: {} chars", body.len());
    println!("DEBUG: Issue labels: {labels:?}");

    let issue = octocrab
        .issues(&github_config.owner, &github_config.repo)
        .create(&title)
        .body(&body)
        .labels(Some(labels))
        .send()
        .await?;

    println!(
        "Created GitHub issue #{} for {} error in {}",
        issue.number, error_type, test_name
    );
    Ok(())
}

async fn handle_error_with_github(
    _github_config: &GitHubConfig,
    error_type: &str,
    error_message: &str,
    test_name: &str,
    _query: &str,
    _schema: &str,
    _generated_rust_code: &str,
) -> Result<()> {
    let error_hash = generate_error_hash(error_type, error_message, test_name);

    println!(
        "DEBUG: Handling error with GitHub - Type: {error_type}, Test: {test_name}, Hash: {error_hash}"
    );

    // match check_issue_exists(github_config, &error_hash).await {
    //     Ok(exists) => {
    //         println!("DEBUG: Issue exists check result: {exists}");
    //         if !exists {
    //             println!("DEBUG: Creating new GitHub issue...");
    //             if let Err(e) = create_github_issue(
    //                 github_config,
    //                 error_type,
    //                 error_message,
    //                 test_name,
    //                 &error_hash,
    //                 query,
    //                 schema,
    //                 generated_rust_code,
    //             )
    //             .await
    //             {
    //                 eprintln!("Failed to create GitHub issue: {e}");
    //             }
    //         } else {
    //             println!(
    //                 "Issue already exists for {error_type} error in {test_name} (hash: {error_hash})"
    //             );
    //         }
    //     }
    //     Err(e) => {
    //         eprintln!("Failed to check existing issues: {e}");
    //         // Try to create the issue anyway if we can't check for duplicates
    //         println!("DEBUG: Attempting to create issue despite check failure...");
    //         if let Err(e) = create_github_issue(
    //             github_config,
    //             error_type,
    //             error_message,
    //             test_name,
    //             &error_hash,
    //             query,
    //             schema,
    //             generated_rust_code,
    //         )
    //         .await
    //         {
    //             eprintln!("Failed to create GitHub issue: {e}");
    //         }
    //     }
    // }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    let matches = ClapCommand::new("queries-test")
        .about("Process helix test directories")
        .arg(
            Arg::new("test_name")
                .help("Specific test directory name to process")
                .value_parser(clap::value_parser!(String))
                .required(false),
        )
        .arg(
            Arg::new("batch")
                .long("batch")
                .help("Enable batch processing with total batches and current batch")
                .num_args(2)
                .value_names(["TOTAL_BATCHES", "CURRENT_BATCH"])
                .value_parser(clap::value_parser!(u32))
                .required(false),
        )
        .arg(
            Arg::new("branch")
                .long("branch")
                .help("Branch to process")
                .value_parser(clap::value_parser!(String))
                .required(false),
        )
        .get_matches();

    let current_dir = env::current_dir().context("Failed to get current directory")?;
    let tests_dir = current_dir.join("tests");

    if !tests_dir.exists() {
        bail!("Tests directory not found at: {}", tests_dir.display());
    }

    // Initialize GitHub configuration (optional - will print warning if not available)
    let github_config = match GitHubConfig::from_env() {
        Ok(config) => {
            println!(
                "GitHub integration enabled for {}/{}",
                config.owner, config.repo
            );
            Some(config)
        }
        Err(e) => {
            println!("GitHub integration disabled: {e}");
            println!("Set GITHUB_TOKEN environment variable to enable automatic issue creation");
            None
        }
    };

    // pull repo to copy to all folders
    let temp_repo = env::temp_dir().join("temp_repo");
    if !temp_repo.exists() {
        fs::create_dir_all(&temp_repo)
            .await
            .context("Failed to create temp directory")?;
    }

    // copy source code from project root to temp_repo
    let project_root = match current_dir.parent() {
        Some(parent) if parent.join("helix-cli").exists() => parent.to_path_buf(),
        Some(_) if current_dir.join("helix-cli").exists() => current_dir.to_path_buf(),
        Some(parent) => bail!("Error: Failed to get project root: {}", parent.display()),
        None => bail!("Error: Failed to get project root"),
    };
    copy_dir_recursive(&project_root, &temp_repo).await?;

    // build rust cli from ./helix-db/helix-cli with sh build.sh dev
    println!("DEBUG: Building rust cli from ./helix-db/helix-cli with sh build.sh dev");
    let build_script_path = project_root.join("helix-cli/build.sh");
    println!("DEBUG: Build script path: {}", build_script_path.display());
    println!("DEBUG: Build script exists: {}", build_script_path.exists());

    let helix_cli_dir = project_root.join("helix-cli");
    println!("DEBUG: Helix CLI dir: {}", helix_cli_dir.display());
    println!("DEBUG: Helix CLI dir exists: {}", helix_cli_dir.exists());

    // Check if helix is already available
    let helix_check = Command::new("helix").arg("--version").output();

    match helix_check {
        Ok(output) => {
            if output.status.success() {
                println!(
                    "DEBUG: Helix already available: {}",
                    String::from_utf8_lossy(&output.stdout)
                );
            } else {
                println!("DEBUG: Helix not available or failed version check");
            }
        }
        Err(e) => {
            println!("DEBUG: Helix command not found: {e}");
        }
    }

    let output = Command::new("sh")
        .arg("build.sh")
        .arg("dev")
        .current_dir(&helix_cli_dir) // Change to helix-cli directory first
        .output()
        .context("Failed to execute build.sh")?;

    println!("DEBUG: build.sh exit code: {:?}", output.status.code());
    println!(
        "DEBUG: build.sh stdout: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    println!(
        "DEBUG: build.sh stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    if !output.status.success() {
        bail!(
            "[FAILED] BUILD FAILED: helix-cli build.sh failed\nStderr: {}\nStdout: {}",
            String::from_utf8_lossy(&output.stderr),
            String::from_utf8_lossy(&output.stdout)
        );
    } else {
        println!("DEBUG: build.sh dev succeeded");

        // Check if helix is available after build
        let helix_check_after = Command::new("helix").arg("--version").output();

        match helix_check_after {
            Ok(output) => {
                if output.status.success() {
                    println!(
                        "DEBUG: Helix available after build: {}",
                        String::from_utf8_lossy(&output.stdout)
                    );
                } else {
                    println!("DEBUG: Helix still not available after build");
                    println!(
                        "DEBUG: Helix version check stderr: {}",
                        String::from_utf8_lossy(&output.stderr)
                    );
                }
            }
            Err(e) => {
                println!("DEBUG: Helix command still not found after build: {e}");
            }
        }
    }

    // Get all test directories
    let mut test_dirs = Vec::new();
    let mut entries = fs::read_dir(&tests_dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_dir()
            && let Some(dir_name) = path.file_name()
            && let Some(name_str) = dir_name.to_str()
        {
            test_dirs.push(name_str.to_string());
        }
    }
    test_dirs.sort();
    println!("Found {} test directories", test_dirs.len());

    if let Some(test_name) = matches.get_one::<String>("test_name") {
        // Process single test directory
        if !test_dirs.contains(test_name) {
            bail!(
                "Error: Test directory '{}' not found. Available tests: {:?}",
                test_name,
                test_dirs
            );
        }

        process_test_directory(test_name, &tests_dir, &temp_repo, &github_config).await?;
        println!("[SUCCESS] Successfully processed {test_name}");
    } else if let Some(batch_args) = matches.get_many::<u32>("batch") {
        // Process in batch mode
        let batch_values: Vec<u32> = batch_args.copied().collect();
        if batch_values.len() != 2 {
            bail!("Error: --batch requires exactly 2 arguments: total_batches current_batch");
        }

        let total_batches = batch_values[0];
        let current_batch = batch_values[1];

        if current_batch < 1 || current_batch > total_batches {
            bail!(
                "Error: Current batch ({}) must be between 1 and {}",
                current_batch,
                total_batches
            );
        }

        if total_batches == 0 {
            bail!("Error: Total batches must be greater than 0");
        }

        // Calculate which tests this batch should process
        let total_tests = test_dirs.len();
        let tests_per_batch = total_tests / total_batches as usize;
        let remainder = total_tests % total_batches as usize;

        // Calculate start and end for this batch
        let start_idx = (current_batch - 1) as usize * tests_per_batch;
        let mut end_idx = current_batch as usize * tests_per_batch;

        // Add remainder tests to the last batch
        if current_batch == total_batches {
            end_idx += remainder;
        }

        // Ensure we don't go out of bounds
        end_idx = end_idx.min(total_tests);

        println!(
            "Processing batch {current_batch}/{total_batches}: tests {}-{} ({})",
            start_idx + 1,
            end_idx,
            test_dirs[start_idx..end_idx].join(", ")
        );

        let tasks: Vec<_> = test_dirs[start_idx..end_idx]
            .iter()
            .map(|test_name| {
                let test_name = test_name.clone();
                let tests_dir = tests_dir.clone();
                let temp_repo = temp_repo.clone();
                let github_config = github_config.clone();
                tokio::spawn(async move {
                    process_test_directory(&test_name, &tests_dir, &temp_repo, &github_config).await
                })
            })
            .collect();

        // Wait for all tasks to complete and collect results
        let mut failed_tests = Vec::new();
        for (i, task) in tasks.into_iter().enumerate() {
            let test_name = &test_dirs[start_idx + i];
            match task.await {
                Ok(Ok(())) => {
                    println!("Successfully processed {test_name}");
                }
                Ok(Err(e)) => {
                    eprintln!("Error processing {test_name}: {e}");
                    failed_tests.push(test_name.clone());
                }
                Err(e) => {
                    eprintln!("Task error for {test_name}: {e}");
                    failed_tests.push(test_name.clone());
                }
            }
        }

        if !failed_tests.is_empty() {
            bail!(
                "[FAILED] BATCH PROCESSING FAILED: {} out of {} tests failed compilation/check: {:?}",
                failed_tests.len(),
                end_idx - start_idx,
                failed_tests
            );
        }

        println!("[SUCCESS] Finished processing batch {current_batch}/{total_batches} successfully");
    } else {
        // Process all test directories in parallel (default behavior)
        println!(
            "Processing all {} test directories in parallel...",
            test_dirs.len()
        );

        let tasks: Vec<_> = test_dirs
            .iter()
            .map(|test_name| {
                let test_name = test_name.clone();
                let tests_dir = tests_dir.clone();
                let temp_repo = temp_repo.clone();
                let github_config = github_config.clone();
                tokio::spawn(async move {
                    process_test_directory(&test_name, &tests_dir, &temp_repo, &github_config).await
                })
            })
            .collect();

        // Wait for all tasks to complete and collect results
        let mut failed_tests = Vec::new();
        for (i, task) in tasks.into_iter().enumerate() {
            let test_name = &test_dirs[i];
            match task.await {
                Ok(Ok(())) => {
                    println!("Successfully processed {test_name}");
                }
                Ok(Err(e)) => {
                    eprintln!("Error processing {test_name}: {e}");
                    failed_tests.push(test_name.clone());
                }
                Err(e) => {
                    eprintln!("Task error for {test_name}: {e}");
                    failed_tests.push(test_name.clone());
                }
            }
        }

        if !failed_tests.is_empty() {
            bail!(
                "[FAILED] PROCESSING FAILED: {} out of {} tests failed compilation/check: {:?}",
                failed_tests.len(),
                test_dirs.len(),
                failed_tests
            );
        }

        println!(
            "[SUCCESS] Finished processing all {} tests successfully",
            test_dirs.len()
        );
    }

    Ok(())
}

async fn process_test_directory(
    test_name: &str,
    tests_dir: &Path,
    temp_repo: &Path,
    github_config: &Option<GitHubConfig>,
) -> Result<()> {
    let folder_path = tests_dir.join(test_name);

    if !folder_path.exists() {
        // Skip non-existent directories silently in parallel mode
        return Ok(());
    }


    // Find the query file - could be queries.hx or file*.hx
    let mut query_file_path = None;
    let schema_hx_path = folder_path.join("schema.hx");

    // First check for queries.hx
    let queries_hx_path = folder_path.join("queries.hx");
    if queries_hx_path.exists() && queries_hx_path.is_file() {
        query_file_path = Some(queries_hx_path);
    } else {
        // Look for file*.hx pattern
        let entries = fs::read_dir(&folder_path).await?;
        let mut entries = entries;
        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();
            if let Some(file_name) = path.file_name()
                && let Some(name_str) = file_name.to_str()
                && name_str.starts_with("file")
                && name_str.ends_with(".hx")
                && !name_str.contains("schema")
            {
                query_file_path = Some(path);
                break;
            }
        }
    }

    // Skip if no query file found or if it's empty
    if let Some(ref query_path) = query_file_path {
        let content = fs::read_to_string(query_path).await?;
        if content.is_empty() {
            return Ok(());
        }
    } else {
        // No query file found, skip this test
        return Ok(());
    }

    // Create a temporary directory for this test
    let temp_dir = env::temp_dir().join(format!("helix_temp_{test_name}"));
    if temp_dir.exists() {
        fs::remove_dir_all(&temp_dir)
            .await
            .context("Failed to remove existing temp directory")?;
    }
    fs::create_dir_all(&temp_dir)
        .await
        .context("Failed to create temp directory")?;

    // Copy the test files (queries.hx, schema.hx, helix.toml, etc.) to temp directory
    copy_dir_recursive(&folder_path, &temp_dir).await?;

    // Copy the entire helix-db project structure for cargo check
    // But skip .helix directory to avoid conflicts
    let helix_db_dir = temp_dir.join("helix-db");
    fs::create_dir_all(&helix_db_dir).await?;

    // Copy all project crates and dependencies (excluding hql-tests to avoid conflicts)
    let crates_to_copy = vec![
        "helix-container",
        "helix-db",
        "helix-macros",
        "helix-cli",
        "metrics",
    ];

    for crate_name in crates_to_copy {
        let src = temp_repo.join(crate_name);
        let dst = helix_db_dir.join(crate_name);
        if src.exists() {
            copy_dir_recursive(&src, &dst).await?;
        }
    }

    // Copy root Cargo.toml and Cargo.lock, but remove hql-tests from workspace
    let cargo_toml_src = temp_repo.join("Cargo.toml");
    let cargo_toml_dst = helix_db_dir.join("Cargo.toml");
    if cargo_toml_src.exists() {
        // Read the Cargo.toml and remove hql-tests from workspace members
        let cargo_content = fs::read_to_string(&cargo_toml_src).await?;
        let modified_content = cargo_content.replace("    \"hql-tests\",\n", "");
        fs::write(&cargo_toml_dst, modified_content).await?;
    }

    let cargo_lock_src = temp_repo.join("Cargo.lock");
    let cargo_lock_dst = helix_db_dir.join("Cargo.lock");
    if cargo_lock_src.exists() {
        fs::copy(&cargo_lock_src, &cargo_lock_dst).await?;
    }

    // Run helix compile command
    let compile_output_path = temp_dir.join("helix-db/helix-container/src");
    fs::create_dir_all(&compile_output_path)
        .await
        .context("Failed to create compile output directory")?;

    let output = Command::new("helix")
        .arg("compile")
        .arg("--path")
        .arg(&temp_dir)
        .arg("--output")
        .arg(&compile_output_path)
        .output()
        .context("Failed to execute helix compile command")?;

    println!(
        "DEBUG: Helix compile output: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    println!(
        "DEBUG: Helix compile stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    if !output.status.success() {
        fs::remove_dir_all(&temp_dir).await.ok();
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        // For helix compilation, we'll show the raw output since it's not cargo format
        let error_message =
            format!("[FAILED] HELIX COMPILE FAILED for {test_name}\nStderr: {stderr}\nStdout: {stdout}");

        // Create GitHub issue if configuration is available
        if let Some(config) = github_config {
            println!("DEBUG: Helix compilation failed in parallel mode, creating GitHub issue...");
            let query_content = if let Some(ref query_path) = query_file_path {
                fs::read_to_string(query_path).await.map_err(|e| {
                    println!("DEBUG: Failed to read query file: {e}");
                    e
                })?
            } else {
                String::new()
            };
            let schema_content = fs::read_to_string(&schema_hx_path).await.map_err(|e| {
                println!("DEBUG: Failed to read schema.hx: {e}");
                e
            })?;
            let generated_rust_code = fs::read_to_string(&compile_output_path.join("queries.rs"))
                .await
                .unwrap_or_else(|_| String::from("Failed to read generated queries.rs"));
            handle_error_with_github(
                config,
                "Helix Compilation",
                &error_message,
                test_name,
                &query_content,
                &schema_content,
                &generated_rust_code,
            )
            .await?;
        } else {
            println!("DEBUG: GitHub integration not configured, skipping issue creation");
        }

        bail!("Error: {}", error_message);
    }

    // Run cargo check on the helix container path
    let helix_container_path = temp_dir.join("helix-db/helix-container");
    if helix_container_path.exists() {
        let output = Command::new("cargo")
            .arg("check")
            .current_dir(&helix_container_path)
            .output()
            .context("Failed to execute cargo check")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let _stdout = String::from_utf8_lossy(&output.stdout);
            // let filtered_errors = extract_cargo_errors(&stderr, &stdout);
            let error_message = format!("[FAILED] CARGO CHECK FAILED for {test_name}\n{stderr}");

            // Create GitHub issue if configuration is available
            if let Some(config) = github_config {
                println!("DEBUG: Cargo check failed in parallel mode, creating GitHub issue...");
                let query_content = if let Some(ref query_path) = query_file_path {
                    fs::read_to_string(query_path).await.map_err(|e| {
                        println!("DEBUG: Failed to read query file: {e}");
                        e
                    })?
                } else {
                    String::new()
                };
                let schema_content = fs::read_to_string(&schema_hx_path).await.map_err(|e| {
                    println!("DEBUG: Failed to read schema.hx: {e}");
                    e
                })?;
                let generated_rust_code =
                    fs::read_to_string(&compile_output_path.join("queries.rs"))
                        .await
                        .unwrap_or_else(|_| String::from("Failed to read generated queries.rs"));
                handle_error_with_github(
                    config,
                    "Cargo Check",
                    &error_message,
                    test_name,
                    &query_content,
                    &schema_content,
                    &generated_rust_code,
                )
                .await?;
            } else {
                println!("DEBUG: GitHub integration not configured, skipping issue creation");
            }
            fs::remove_dir_all(&temp_dir).await.ok();
            bail!("Error: {}", error_message);
        }
    }

    println!("Cargo check passed for {test_name}");
    // Clean up temp directory
    fs::remove_dir_all(&temp_dir).await.ok();

    Ok(())
}

async fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
    if !dst.exists() {
        fs::create_dir_all(dst).await?;
    }

    let mut entries = fs::read_dir(src).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        let file_name = path.file_name().unwrap();
        let dest_path = dst.join(file_name);

        if path.is_dir() {
            if IGNORE_DIRS.contains(&path.file_name().unwrap().to_str().unwrap()) {
                continue;
            }
            Box::pin(copy_dir_recursive(&path, &dest_path))
                .await
                .map_err(|e| {
                    println!("DEBUG: Failed to copy directory {}: {}", path.display(), e);
                    e
                })?;
        } else {
            fs::copy(&path, &dest_path).await.map_err(|e| {
                println!("DEBUG: Failed to copy file {}: {}", path.display(), e);
                e
            })?;
        }
    }

    Ok(())
}

const IGNORE_DIRS: [&str; 3] = ["target", ".git", ".helix"];
